IOS12+Swift4+Xcode10开发 - 4 元气天气APP

项目介绍

app6的副本

需求分析

iOS原生风格的极简的日程管理工具。

现有的Todo工具:UI设计趋向于小清新、可爱动画等多元设计;功能为可以记录时间、地点、时间重要程度等信息,这样对于提高了增加任务的时间、精力成本。

本APP主打:界面使用iOS原生风格,随时想到、随时记录。

功能:用户可以对待办事项输入、修改、删除、修改顺序操作,可以标记已经完成任务。

使用场景:午餐时想到了一个idea直接打开APP记录、午睡后想到这个idea的实现可直接修改,晚上实现该功能后直接打钩,第二天测试人员告知该idea可行时删除任务。

功能需求

浏览任务页面:可以在tableview查看所有任务及其完成状态;可以在编辑状态下批量删除任务、移动任务顺序;按下navigation的+进入添加任务页面;按下>进入修改任务页面。

添加修改页面:判断当前任务是添加还是修改。使用自定义protocol和delegate反向传值,将文字传回浏览页面。

知识点

主线程

https://www.jianshu.com/p/f042432e2d7d

TableView的使用

1.我们知道tableView是IOS中的高级视图,其继承与ScrollView,具有ScrollView的功能,还扩展了许多。

2.tableView的表现格式分两种Plain和Grouped两种风格

3.tableView的两种代理类delegate和dataSource.这两种代理至关重要,我们在做tableView和这些代理是分不开的。

4.代理中比较常用的代理方法:

(1)dataSource的两个必须使用的代理

显示UITableView的Cell的个数:一共有多少个格子

Cell和model的数据的交互:根据数据,获取每一个数据的值

5.增删改查需要做的:第一步获取需要操作的行数(增加不需要),第二步对数据库进行处理,第三部更新视图

6.更新视图的2种方式:beginUpdates、endUpdates之前操作(如一行一行的删除数据);tableView.reloadData()刷新TableView了。

在使用数据库之后,我就直接reloadData()刷新TableView了。比较方便

1
2
3
4
5
6
// 方式一:把批量对视图的操作防在两句话中间,可以提高app的性能
tableView.beginUpdates()
// codes
tableView.endUpdates()
// 方式二:从新load数据更新页面;但没有动画效果 —— 就是相当于执行 cell for row的方法,将结果取出来,再更新视图
//tableView.reloadData()

常用控件:相当于一个扑克盒子,里面的页面是出栈和入栈来实现试图切换。

利用segue界面跳转一共有两种方式:

第一种就是以上我的例子,利用button组件,拖拽添加segue,直接运行就可以用。

第二种是利用ViewController与ViewController之间,拖拽添加segue。不过,这种方法就需要在相应需要跳转的方法内写入代码,手动去设置它的跳转。

iOS 10中有几种sugue类型:

Show:新的view controller将被添加view controller栈的顶部。跳转的页面有Navigation bar,并且有返回原来页面的返回按钮。这是非常常用的的类型。

Show detail:在view controller栈中,新的view controller将被替代原来的view controller。跳转的页面没有Navigation bar,也没有返回原来页面的返回按钮。

Present modally:新页面将以动画形式从底部出现到覆盖整个手机屏幕。这种形式最常见的列子是iOS自带的日历应用:

项目布局

UI设计

页面一

页面二

步骤

配置tableview数据

重用单元格的形式,数据成千上万行,最终渲染个数为屏幕上显示的数目。

往下拉的时候,最上面的cell到最下面来,放置重复渲染,提高手机性能。

使用tableview cell

Cell定一个class为TodoCell,Cell使用as!强制转换为TodoCell。

model结构体使用Struct初探

结构体:不需要写初始化构造器,直接可以构造出来;轻量级的class

indexPath动态配置每行数据

确定位置:第几行indexPath.row、第几段indexPath.session

cell切换打勾和取消

使用static单元格,grouped风格。

image-20190308145035169

navigation controller类似扑克盒子

image-20190308145327440

跳转实现

image-20190308145858073

image-20190308150415699

主页面标题设置为大标题,需要在选中navigation页面,设置大标题

image-20190308150714710

新页面需要设置小标题,添加item当做标题,再修改名称,改为大标题为never

image-20190308151137906

返回按钮的名称需要在页面一的任务清单UINavigationItem中修改

image-20190308153255753

添加任务功能✨

流程如下:

image-20190308153815308

使用:(自定义protocol和delegate反向传值)+出栈

添加新任务:反向传值

确定之后页面消失:出栈,navigation老大让他出栈

编辑任务✨

1 正向传值segue将选中的cell中的(1 任务文本todo;2选中的行数row)传到编辑页面

2 编辑页面根据输入框判断是否有文本,定下title。若不为空,title为编辑任务;为空title为添加任务

3 修改后按下navigate bar button确认后,将(1修改后的文字;2之前选中的行数)传回主页面

4 主页面中实现协议的函数中:修改mode数组中的数据,根据行数row找到cell,再将cell的文字改为新的任务

image-20190308170849569

左滑删除

已经预置代码,直接取消注释;

添加上删除某一行的数据;

更新页面已经写了,不需要添加。

批量选择+批量删除

navigation自带了添加编辑button的代码,取消注释,将button改到左边即可,还需要批量功能

image-20190308211044994

由于需要批量的对象是table view,选中他,找到edit选择multiple 编辑即可

image-20190308211257625

实现批量选中的效果:

image-20190308211343078

批量删除需要重新放一个”删除“按钮;

在按下按钮后:获取选中的indexPath存储于数组,通过循环删除数据对应行数indexPath.row,使用table view的delete方法删除存于数组中的行数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取所有被选中的indexPath,若为空则不进行删除操作
if let indexPaths = tableView.indexPathsForSelectedRows{
//1 Model 数据删除
for indexPath in indexPaths{
todos.remove(at: indexPath.row)
}
//2 View 视图更新—— table View的删除方法
// 方式一:把批量对视图的操作防在两句话中间,可以提高app的性能
tableView.beginUpdates()
tableView.deleteRows(at: indexPaths, with: .automatic)
tableView.endUpdates()
// 方式二:从新load数据更新页面;但没有动画效果
//tableView.reloadData()
}

ps 更新视图

// 方式一:把批量对视图的操作防在两句话中间,可以提高app的性能
tableView.beginUpdates()
// codes
tableView.endUpdates()
// 方式二:从新load数据更新页面;但没有动画效果 —— 就是相当于执行 cell for row的方法,将结果取出来,再更新视图
//tableView.reloadData()

发现无法选择,原因是:原先的设置选择后,立即变成未选中状态,使得页面保持白色,不会保持变成灰色。需要:在原先的选择代码中,使用flag——isEditing,判断若在editing状态,则可以选中、打钩等一系列操作。

1
2
3
4
// 如果不是在编辑的情况下,选择打勾才有用、取消选中等才有用
if !isEditing{
//选择的代码——选择后原数据状态改变、重新mark为相反的、判断mark绘制√,取消选择背景变回白色
}

按钮的默认文字汉化

左滑时候的delete —— 变成 ”删除“的关键词:tableView、title、delete、button后出现需要重写的方法

批量选择edit、done改成中文——1 通过command+点击获取方法、2通过三元运算符+isEditing确认是”编辑“还是”确定“

移动单元格

已有该方法,重写to support rearranging the table view

首先使用临时变量temp交换两个数据,接着更新视图moveRow(不写这一句也可以移动,即自动调用)。

但是这里存在一个小bug,当编辑状态下,当选中数据,被移动后,该数据将一直处于选中状态,且无法删除。结局方法为:移动数据后,reloadData,取消选中状态,就不会导致setEditing函数自动调用该函数,导致混乱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 移动单元格功能
// Override to support rearranging the table view.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
// 1 移动数据
let temp = todos[fromIndexPath.row]
todos[fromIndexPath.row] = todos[to.row]
todos[to.row] = temp
print(todos)

// 2 更新数据:自动更新,可以不用下面的代码
tableView.moveRow(at: fromIndexPath, to: to)
// 3 移动后,刷新
tableView.reloadData()
}

简单数据存储userDefaults

AppDelegate文件+轻量级本地存储userDefaults+编码解码

1 AppDelegate文件:APP的声明周期:

打开:APP启动、激活

按下home健:由于某种原因被挤到后台、进入后台

再次进入:即将激活、激活

退出:(APPlist 中)进入后台、(退出后)完全退出

2 数据

数据存储在沙盒里面,信息安全,本APP只能访问本APP的数据

沙盒数据中只能存储基本的数据类型,我们的数据是存储对象的Array,无法存储进沙盒plist,因此需要写一个存储函数来编码数据为.data

3 使用userDefaults

写好储存数据的方法(编码为data格式+存入沙盒)和解码方法(通过forKey获取数据,解码至todos数组):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 数据存储——编码数据
func saveData(){
// 可能会抛出错误
do{
// 编码的固定格式 —— 创建一个编码器(JSONENcoder)——要在model中遵守协议使对象可编码
let data = try JSONEncoder().encode(todos)
// 得到data类型的数据,存入沙盒
UserDefaults.standard.set(data, forKey: "todos")
}catch{
print(error)
}
}
//取数据——解码
func getData(){
// 获得data类型的数据,由于可能为空,则使用if let:不为空的时候获取本地数据,为空则不获取
if let data = UserDefaults.standard.data(forKey: "todos") {
// 解码固定格式
do{
// 解码的固定格式 —— 创建一个编码器(JSONENcoder)——要在model中遵守协议使对象可编码
todos = try JSONDecoder().decode([Todo].self, from: data)
}catch{
print(error)
}
}
}

再把写数据的函数在所有操作数据处理调用;

在程序load时候调用取数据。

本地存储coredata和realm

了解

cs和bs+什么是数据库+为什么App需要本地存储

cs:client-server —— QQ、微信——聊天记录存在本机

bs:browser-sever —— 网页论坛——数据存储在服务器

IOS本地存储数据库:

userdefaults——轻量级的数据

coredata——苹果自带的,学习成本高,代码多、不够快——了解

realm:速度快

realm安装与示例使用

由于cocoapods无法pod,我选择下载安装法,自行配置:

先去 Realm 的官网去下载最新框架: https://realm.io/cn/docs/swift/latest/#prerequisites

接着拖拽 RealmSwift.frameworkRealm.framework 文件到”Embedded Binaries”选项中。选中 Copy items if needed 并点击 Finish

image-20190309140620794

寻找应用的 Realm 文件:Swift using Realm Swift:
(lldb) po Realm.Configuration.defaultConfiguration.fileURL

command+shift+g :跳转窗口,进入沙盒

在model中新建模型User.swift,import包,定义模型

1
2
3
4
5
6
7
import Foundation
import RealmSwift
// 定义模型的做法和定义常规 Swift 类的做法类似
class User: Object {
@objc dynamic var name = ""
@objc dynamic var age = 0
}

进入AppDelegate,找到启动的方法,在该方法中:

1 数据实例化

do - catch中 2 创建数据库、3 存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
print("APP启动")
// 0 输出沙盒地址
print(Realm.Configuration.defaultConfiguration.fileURL)

// 1 实例化需要存储数据 —— 实例化user对象
let user = User()
user.name = "Iris"
user.age = 18

do{
// 2 创建一个数据库
let realm = try Realm()
// 3将对象存入数据库
try realm.write {
realm.add(user)
}
}catch{
print(error)
}
return true
}

查看数据:print(Realm.Configuration.defaultConfiguration.fileURL)

command+shift+g :输入得到的地址,跳转窗口,进入沙盒,查看的结果

image-20190309145355605

realm操作

1 在主页面处实例化数据库,为全局变量

1
2
3
// 使用数据库后,todo相当于一个中间人的角色
// 实例化realm数据库 —— 强制try,确定不会出异常 (一般不这样写,可能会出现网络等异常)
let realm = try! Realm()

2 创建RealmSwift的类 class Todo

1
2
3
4
5
6
7
import Foundation
import RealmSwift
//定义模型的做法和定义常规 Swift 类的做法类似
class Todo: Object {
@objc dynamic var name = ""
@objc dynamic var checked = false
}

2 写数据

新增数据功能:

新增数据后,创建一个类Todo的todo对象,传入saveData

3 取数据

现在viewLoad()的时候,读取数据。发现需要results类型,所以下一步更改todos的类型

1
2
// 从Realm数据库中读取数据
todos = realm.objects(Todo.self)

声明todos由数组类型改为复合类型,接受取出来的 result array的数据,定义为可选性,可能为空

1
2
3
4
5
6
7
8
9
//定义为esults类型todos -- 定义复合数据类型 -- 名字:复合类型<类型> ——1种方式
var todos: Results<Todo>?
//定义空数组L:需要指明数组内存的类型。—— 3种方式
//var Todos: Array<Todo>=[]
//var todos:[Todo] = []
//var todos = [Todo]()
//定义空字典的方法:指明key和value的类型; [:]表明m空字典。—— 2 种方式
//var dict:[String:Any] = [:]
//var dict = [String:Any]()

可选性(todos为空),则初始的时候判断。

为此,进行初始判断:当todos取出来为空的时候,数组的行数为0,且出现文字——请添加任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 二、每段有几行:第i个session的行数
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
// todos?.count :?表示,该符号前面为空,则不执行后面的,整个则为 nil
// 空合运算符 xxx ?? ooo ,xxx为空,则执行ooo
// 若tosos为空,则ession的行数 返回1
return todos?.count ?? 0
}
// 三、每行显示什么
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 想要用到的到TodoCell下面的属性,向下z强制转型
let cell = tableView.dequeueReusableCell(withIdentifier: "todo", for: indexPath) as! TodoCell
// 若取出的todos不为空,我们赋值给 这里的todos,可以在下面用
if let todos = todos{
//确定位置 第几行indexPath.row、第几段indexPath.session
// Configure the cell...
cell.todo.text = todos[indexPath.row].name
cell.checkMark.text = todos[indexPath.row].checked ? "✓" : " "
}

在存储数据的时候,把更新页面直接改为reload()方法。

修改数据

1 修改任务部分 :获取当前行数,修改todos![indexPath.row].name = name;更新视图

2 状态更改部分:按下,状态取反,修改todos![indexPath.row].checked =取反;更新视图

删数据

删除数据

更新视图

1
2
3
4
5
6
7
8
9
10
11
12
13
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
// 数据操作
do{
try realm.write {
realm.delete(todos![indexPath.row])
}
}catch{
print(error)
}
// 更新视图
tableView.reloadData()
}

Realm-搜索和排序+searchBar+收起软键盘+主线程

点击search bar 搜索,发生什么事情。

想到需要一个delegate协议。先遵守协议UISearchBarDelegate,再委托。告诉Todos Controller,search bar是老板(委托人),一会他有些申请会委托给controller做。

image-20190310122008115

1
2
3
4
5
6
7
8
9
10
11
12
13
// 实现搜索功能
// 实现searchBar委托给todosController干的事情
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
// 1.1 从数据库获取所有的数据
let todosData = realm.objects(Todo.self)
// 1.2 从所有数据中获取查询后的数据
// predicate——断言——规定怎么查询
// name 中包含 %@ 占位符 _ searchBar.text!强制解包
todos = todosData.filter("name CONTAINS %@", searchBar.text!)

// 2 更新视图
tableView.reloadData()
}

当searchBar内容改变的时候,查看其是否为空,若为空,则‘’‘

搜索之后,根据创建时间排序。先在class 中添加时间属性为当前时间,搜索的时候按照时间倒叙:

1
2
3
4
5
6
7
8
9
10
11
12
// 实现searchBar委托给todosController干的事情
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
// 1.1 从数据库获取所有的数据
let todosData = realm.objects(Todo.self)
// 1.2 从所有数据中获取查询后的数据
// predicate——断言——规定怎么查询
// name 中包含 %@ 占位符 _ searchBar.text!强制解包
// 排序:时间倒叙
todos = todosData.filter("name CONTAINS %@", searchBar.text!).sorted(byKeyPath: "createdAT", ascending: false)
// 2 更新视图
tableView.reloadData()
}

收起键盘:学习:https://www.jianshu.com/p/f042432e2d7d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 二、在搜索栏清空后,需要显示所有数据,且收起键盘
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// 看是否为空
if searchBar.text!.isEmpty{
// 1.1 从数据库获取所有的数据
let todosData = realm.objects(Todo.self)
todos = todosData
// 2 更新视图
tableView.reloadData()
// 让用户界面在主线程上进行——优先执行
// 经常用于使UI方面的操作提前执行,查询可以慢慢查,我们的键盘先收起来——用户体验更好
DispatchQueue.main.async {
// 3.收起键盘 让searchBar失去焦点(光标消失+软件盘收起)
searchBar.resignFirstResponder()
}
}
}

未完成:

//需求:打√之后,就放置在最后,取消打钩,放置在最前

源码

1
2